﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Core.Parsers
{
    class Response
    {
        private MemoryStream ms;
        private BinaryReader br;
        private List<Model.Elements.HttpHeader> lstHeaders;
        private Model.Elements.HttpResponsePacket packet;

        public bool error = false;

        public Response()
        {
            lstHeaders = new List<Model.Elements.HttpHeader>();
        }

        public static bool IsValidPacket(byte[] packet)
        {
            Response r = new Response();
            r.Parse(packet);
            return !r.error;
        }

        private bool IsChunked()
        {
            bool hasChunkedTransferEncoding = false;
            for (int iHeader = 0; iHeader < lstHeaders.Count; iHeader++)
            {
                Model.Elements.HttpHeader header = lstHeaders[iHeader];

                if (header.name.ToLower() == "transfer-encoding" && header.value.ToLower() == "chunked")
                {
                    hasChunkedTransferEncoding = true;
                    //lstHeaders.RemoveAt(iHeader);
                    break;
                }
            }

            return hasChunkedTransferEncoding;
        }

        private bool IsGZiped()
        {
            bool gzip = false;
            for (int iHeader = 0; iHeader < lstHeaders.Count; iHeader++)
            {
                Model.Elements.HttpHeader header = lstHeaders[iHeader];

                if (header.name.ToLower() == "content-encoding" && header.value.ToLower() == "gzip")
                {
                    gzip = true;
                    break;
                }
            }

            return gzip;
        }

        private void RemoveHeader(string headerName)
        {
            for (int iHeader = 0; iHeader < lstHeaders.Count; iHeader++)
            {
                Model.Elements.HttpHeader header = lstHeaders[iHeader];

                if (header.name.ToLower() == headerName.ToLower())
                {
                    lstHeaders.RemoveAt(iHeader);
                    break;
                }
            }
        }

        private void AddHeader(string name, string value)
        {
            lstHeaders.Add(new Model.Elements.HttpHeader(name, value));
        }

        public void Parse(byte[] packet)
        {
            ms = new MemoryStream(packet);
            br = new BinaryReader(ms);

            if (ms.Length == 0)
            {
                error = true;
                return;
            }

            string version = string.Empty;
            string errorCode = string.Empty;
            string message = string.Empty;

            ProcessFirstLine(out version, out errorCode, out message, out error);
            if (error)
                return;

            ProcessHeaders(out lstHeaders, out error);
            if (error)
                return;

            int length = GetContentLenght(out error);
            if (error)
                return;

            byte[] data = null;
            bool chunked = false;
            bool gzip = false;

            chunked = IsChunked();
            gzip = IsGZiped();

            if (chunked)
                RemoveHeader("Transfer-Encoding");
            if (gzip)
                RemoveHeader("Content-Encoding");

            // If it doesn't have content-length """AND NO CHUNCKS""" it must end with 0x0d 0x0a 0x0d 0x0a. In other case isn't a valid response
            if (length == -1 && !chunked)
            {
                if (packet[packet.Length - 0x04] != 0x0d ||
                    packet[packet.Length - 0x03] != 0x0a ||
                    packet[packet.Length - 0x02] != 0x0d ||
                    packet[packet.Length - 0x01] != 0x0a)
                {
                    error = true;
                    return;
                }
            }

            // if length is -1 is because the server didn't send any content-length
            if (length > -1)
                data = ProcessData(0, length, out error);
            else
            {
                if (chunked) // It has to read the data in chuncks -> http://en.wikipedia.org/wiki/Chunked_transfer_encoding
                {
                    data = ProcessDataByChunks(0, out error);
                    if (error)
                        return;
                    AddHeader("Content-Length", data.Length.ToString());
                }
                else // When there are no chunks and no content-length it reads until the end
                    data = ProcessDataUnknowLength(0, out error);
            }

            if (error)
                return;

            if (gzip)
            {
                // decompress data
                System.IO.Compression.GZipStream gzipStream = new System.IO.Compression.GZipStream(new MemoryStream(data), System.IO.Compression.CompressionMode.Decompress);


                byte[] bufferDecompress = new byte[1024];
                MemoryStream memoryStream = new MemoryStream();
                int count = 0;
                do
                {
                    count = gzipStream.Read(bufferDecompress, 0, bufferDecompress.Length);
                    if (count > 0)
                        memoryStream.Write(bufferDecompress, 0, count);
                }
                while (count > 0);
                data = memoryStream.ToArray();
                memoryStream.Close();

                // Now its time to re-adjust the content length if exists
                bool contentLengthAsigned = false;
                Model.Elements.HttpHeader header = null;
                for (int iHeaderContentLength = 0; iHeaderContentLength < lstHeaders.Count; iHeaderContentLength++)
                {
                    header = lstHeaders[iHeaderContentLength];
                    if (header.name.ToLower() == "content-length")
                    {
                        header.value = data.Length.ToString();
                        contentLengthAsigned = true;
                        break;
                    }
                }
                if (!contentLengthAsigned)
                    lstHeaders.Add(new Model.Elements.HttpHeader("Content-Length", data.Length.ToString()));

            }

            this.packet = ReassemblePacket(version, errorCode, message, lstHeaders, data);

        }

        private byte[] ReadRawData(int length)
        {
            List<byte> data = new List<byte>();
            int bufferLen = 512 * 10;
            byte[] buffer = new byte[bufferLen];

            bool end = false;
            do
            {

                int bytesReaded = 0;
                try
                {
                    buffer = new byte[bufferLen];
                    bytesReaded = br.Read(buffer, 0, bufferLen);
                }
                catch
                {

                }

                if (bytesReaded == 0)
                    end = true;
                else
                {
                    Array.Resize(ref buffer, bytesReaded);
                    data.AddRange(buffer);
                }
            } while (!end);

            return data.ToArray();
        }

        public Model.Elements.HttpResponsePacket GetPacket()
        {
            return this.packet;
        }

        private void ProcessFirstLine(out string version, out string code, out string message, out bool error)
        {
            //VERSION CODE MESSAGE

            /// OUT PARAMETERS
            error = false;
            version = string.Empty;
            code = string.Empty;
            message = string.Empty;

            ms.Seek(0, SeekOrigin.Begin);

            string line = br.ReadLine();

            string[] splitedLine = line.Split(' ');

            if (splitedLine.Length == 2)
            {
                version = splitedLine[0];
                code = splitedLine[1];
                message = string.Empty;
            }
            else if (splitedLine.Length >= 3)
            {
                version = splitedLine[0];
                code = splitedLine[1];
                message = line.Substring(version.Length + code.Length + 2, line.Length - (version.Length + code.Length + 2));
            }
            else
            {
                error = true;
                return;
            }
        }

        private void ProcessHeaders(out List<Model.Elements.HttpHeader> lstHeaders, out bool error)
        {
            error = false;
            lstHeaders = new List<Model.Elements.HttpHeader>();

            string line = string.Empty;
            while (!string.IsNullOrEmpty(line = br.ReadLine()))
            {

                if (!line.Contains(':'))
                {
                    error = true;
                    return;
                }

                string nameHeader = line.Split(':')[0];
                string valueHeader = line.Substring(nameHeader.Length + 1).Trim();

                Model.Elements.HttpHeader header = new Model.Elements.HttpHeader(nameHeader, valueHeader);
                lstHeaders.Add(header);
            }

        }

        private byte[] ProcessData(int offset, int length, out bool error)
        {
            error = false;
            byte[] content = null;

            try
            {

                content = new byte[length];
                int bytesReaded = br.Read(content, 0, length);
                if (bytesReaded != length)
                {
                    error = true;
                    return null;
                }
            }
            catch
            {
                error = true;
                return null;
            }

            return content;
        }

        private byte[] ProcessDataUnknowLength(int offset, out bool error)
        {
            List<byte[]> bytesPacket = new List<byte[]>();

            int bufferLength = 1024;
            byte[] buffer = new byte[bufferLength];

            int readedBytes = 0;
            while ((readedBytes = br.Read(buffer, 0, bufferLength)) > 0)
            {
                byte[] newBuffer = new byte[readedBytes];
                Array.Copy(buffer, newBuffer, readedBytes);
                bytesPacket.Add(newBuffer);
            }

            int sizeFinalBuffer = 0;
            for (int i = 0; i < bytesPacket.Count; i++)
                sizeFinalBuffer += ((byte[])bytesPacket[i]).Length;
            byte[] finalBuffer = new byte[sizeFinalBuffer];

            int position = 0;
            for (int i = 0; i < bytesPacket.Count; i++)
            {
                foreach (byte b in ((byte[])bytesPacket[i]))
                {
                    finalBuffer[position] = b;
                    position++;
                }
            }

            error = false;

            return finalBuffer;
        }

        /// <summary>
        /// Reads the data in chuncks
        ///  http://en.wikipedia.org/wiki/Chunked_transfer_encoding
        /// </summary>
        /// <param name="offset"></param>
        /// <param name="error"></param>
        /// <returns></returns>
        private byte[] ProcessDataByChunks(int offset, out bool error)
        {
            error = false;
            List<byte> bytesPacket = new List<byte>();

            uint bytesToRead = 0;
            bool reading = true;
            do
            {
                string chunkSize = br.ReadLine();
                if (string.IsNullOrWhiteSpace(chunkSize))
                {
                    reading = false;
                    error = true;
                }
                else if ( chunkSize == "0")
                    reading = false;
                else
                {
                    bytesToRead = uint.Parse(chunkSize, System.Globalization.NumberStyles.AllowHexSpecifier);
                    byte[] buffer = br.ReadBytes((int)bytesToRead);
                    if (bytesToRead != buffer.Length)
                    {
                        error = true;
                        break;
                    }

                    byte[] LastBytes = br.ReadBytes(2); // Last 2 bytes of the last chunk
                    // Is mandatory to have this 2 (crlf) bytes at the end of the last chunk
                    if (LastBytes.Length == 2 && LastBytes[0] == 0x0d && LastBytes[1] == 0x0a)
                        bytesPacket.AddRange(buffer);
                    else
                    {
                        error = true;
                        break;
                    }
                }
            }
            while (reading);

            if (bytesPacket.Count == 0)
            {
                error = true;
                return bytesPacket.ToArray();
            }
            return bytesPacket.ToArray();
        }

        private int GetContentLenght(out bool error)
        {
            foreach (Model.Elements.HttpHeader header in lstHeaders)
            {
                if (header.name == "Content-Length")
                {
                    try
                    {
                        int lenght = int.Parse(header.value);
                        error = false;
                        return lenght;
                    }
                    catch
                    {
                        error = true;
                        return -1;
                    }
                }
            }

            error = false;
            return -1;
        }

        private Model.Elements.HttpResponsePacket ReassemblePacket(string version, string errorCode, string message, List<Model.Elements.HttpHeader> lstHeaders, byte[] data)
        {
            Model.Elements.HttpResponsePacket packet = new Model.Elements.HttpResponsePacket(version, errorCode, message);
            foreach (Model.Elements.HttpHeader header in lstHeaders)
                packet.AddHeader(header);
            packet.SetData(data);
            return packet;
        }
    }
}
